package com.onekbase.framework.mybatis.engine.core;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.builder.xml.XMLMapperBuilder;
import org.apache.ibatis.session.Configuration;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

@Slf4j
public class MetaScriptUpdater {

    // sys_init_script表的一项配置，查询所有业务配置的SQL
    private static final String SID_LOAD_ALL_SCRIPT = "meta.config.selectScript";

    // sys_init_script表的一项配置，按sid查业务表的一个SQL配置
    private static final String SID_LOAD_SCRIPT_BY_ID = "meta.config.selectScriptById";

    private static final String SID_LOAD_SCRIPT_BY_VERSION = "meta.config.selectScriptByVersion";

    private static Map<String,Long> versionMap = new HashMap<>();

    private SqlSessionTemplate ss;

    private MetaEngineProperties mep ;

    @Setter
    private StringRedisTemplate redisTemplate;

    public void setSqlSession(SqlSessionTemplate ss) {
        this.ss = ss;
    }

    public void setMetaEngineProperties(MetaEngineProperties mep) {
        this.mep = mep;
    }

    public void updateVersion(String id, Long version){
        versionMap.put(id,version);
    }

    public void updateCacheVersion(String id, Long version){
        if(!mep.isEnableUpdater()){
            return ;
        }
       if(!mep.isEnableRedisCache()) {
           return ;
       }
       redisTemplate.opsForValue().set(id, String.valueOf(version));
    }

    public Long getCacheVersion(String id){
        if(!mep.isEnableRedisCache()) {
            return null;
        }
        String sv = redisTemplate.opsForValue().get(id);
        return sv==null? null : Long.valueOf(sv);
    }

    /**
     * 如果是高并发场景这里有线程安全问题，mybatis的配置对象是线程不安全的
     * 更新配置过程会删除Mybatis配置再重新解析，如果这过程中有对应的SQL配置在获取可能取到错误的信息
     * 方案：可以做成配置化的
     * 1. 方法添加锁，在用redis版本验证情况下对性能影响不大，因为配置很少更新
     * 2. 添加查询标识，一个sid配置在使用中跳过不做配置更新，会影响配置更新的及时性，但影响不大
     * @param sid
     * @return
     */
    public boolean updateScript(String sid){
        try{
            if(!mep.isEnableUpdater()){
                log.info("updateScript updater is unable");
                return false;
            }
            return updateScriptCore(sid);
        }catch (Exception e){
            log.error("updateScript error sid:"+sid ,e);
            return false;
        }
    }

    private boolean updateScriptCore(String sid){
        // 未开启redis缓存管理版本
        if(!mep.isEnableRedisCache()){
            return updateScriptFromDb(sid).isSucess();
        }
        // 开启redis缓存管理版本
        Long cacheVersion = getCacheVersion(sid);
        if(cacheVersion==null){
            // 开启缓存但没有版本信息是不正常情况
            log.warn("updateScript no cache version sid:{}",sid);
            // 缓存无数据从数据库更新
            if(mep.isNoCacheLoadDb()){
                log.warn("updateScript no cache version update from db sid:{}",sid);
                UpdateResult rs = updateScriptFromDb(sid);
                // 版本不为空手工刷一下
                if(rs.getVersion()!=null) {
                    updateCacheVersion(sid, rs.getVersion());
                }
                return rs.isSucess();
            }
            // 缓存无版本数据直接返回
            return false;
        }
        Long jvmVersion = versionMap.getOrDefault(sid,0l);
        if (jvmVersion>=cacheVersion){
            // 不用更新
            log.info("updateScript check cache version sid:{} needUpdate:false jvmVersion:{} cacheVersion:{}",sid,jvmVersion,cacheVersion);
            return false;
        }
        //jvmVersion<cacheVersion 走数据库更新逻辑
        log.info("updateScript check cache version sid:{} needUpdate:true jvmVersion:{} cacheVersion:{}",sid,jvmVersion,cacheVersion);
        return updateScriptFromDb(sid).isSucess();
    }

    @Data
    @AllArgsConstructor
    class UpdateResult{
        private boolean sucess;
        private Long version;
    }

    private UpdateResult updateScriptFromDb(String sid){
        Long jvmVersion = versionMap.getOrDefault(sid,0l);
        Map<String,Object> script = ss.selectOne(SID_LOAD_SCRIPT_BY_ID,sid);
        String sc=String.valueOf(script.getOrDefault("script",""));
        Date updateTime = (Date) script.get("update_time");
        Long dbVersion = updateTime.getTime();
        boolean needUpdate = jvmVersion<dbVersion;
        log.info("updateScriptFromDb check version sid:{} needUpdate:{} jvmVersion:{} dbVersion:{}",sid,needUpdate,jvmVersion,dbVersion);
        if(needUpdate){
            log.info("updateScriptFromDb start sid:{} updateTime:{}", sid ,updateTime);
            boolean rs = parseConfig(sid,sc,ss);
            if(rs) {
                updateVersion(sid, dbVersion);
            }
            log.info("updateScriptFromDb end sid:{} rs:{} updateTime:{}", sid, rs ,updateTime);
            return new UpdateResult(rs,dbVersion);
        }
        return new UpdateResult(false,dbVersion);
    }

    public static boolean parseConfig(String sid,String script, SqlSessionTemplate ss){
        try{
            Field fms = Configuration.class.getDeclaredField("mappedStatements");
            fms.setAccessible(true);
            Map mappedStatements = (Map) fms.get(ss.getConfiguration());
            if(mappedStatements.containsKey(sid)) {
                mappedStatements.remove(sid);
            }
            Field flr = Configuration.class.getDeclaredField("loadedResources");
            flr.setAccessible(true);
            Set loadedResources = (Set) flr.get(ss.getConfiguration());
            if(loadedResources.contains(sid)) {
                loadedResources.remove(sid);
            }
            InputStream is = new ByteArrayInputStream(script.getBytes(StandardCharsets.UTF_8));
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(is, ss.getConfiguration(), sid,
                    ss.getConfiguration().getSqlFragments());
            mapperParser.parse();
            log.info("parseConfig success sid:{}",sid);
            return true;
        }catch (Exception e){
            log.error("parseConfig error sid:"+sid,e);
            return false;
        }
    }

    /**
     * 手工写的一个初始化配置，从sys_init_script加载初始化SQL
     * 从sys_init_script加载的配置不会根据版本更新
     */
    public void loadInitScript(){
        StringBuffer is =new StringBuffer();
        is.append("    <select id=\"selectScript\" resultType=\"java.util.Map\">\n");
        is.append("        select * from sys_init_script\n");
        is.append("    </select>");
        String initConfigScript = buildScript(is.toString(),"meta.init.config");
        String initid = "meta.init.config.selectScript";
        MetaScriptUpdater.parseConfig(initid,initConfigScript,ss);
        List<Map<String,Object>> lstInitScript = ss.selectList(initid);
        lstInitScript.forEach(this::parseScriptConfig);
    }

    private static String getSpaceFromSid(String sid){
        return sid.substring(0,sid.lastIndexOf('.'));
    }
    public static String buildScript(String script,String namespace){
        StringBuffer scb = new StringBuffer();
        scb.append("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>");
        scb.append("<!DOCTYPE mapper\n" +
                "        PUBLIC \"-//mybatis.org//DTD Mapper 3.0//EN\"\n" +
                "        \"https://mybatis.org/dtd/mybatis-3-mapper.dtd\">");
        scb.append("<mapper namespace=\"").append(namespace).append("\">");
        scb.append(script);
        scb.append("</mapper>");
        return scb.toString();
    }

    private void parseScriptConfig(Map<String,Object> script){
        String sid= String.valueOf(script.getOrDefault("sid",""));
        String sc=String.valueOf(script.getOrDefault("script",""));
        Date updateDate = (Date) script.get("update_time");
        String mapper = buildScript(sc,getSpaceFromSid(sid));
        MetaScriptUpdater.parseConfig(sid,mapper,ss);
        updateVersion(sid, updateDate.getTime());
        updateCacheVersion(sid, updateDate.getTime());
        log.info("parseScriptConfig sid:{} updatetime:{}",sid,updateDate);
    }

    /**
     * 加载业务SQL的配置
     */
    public void loadScript(){
        List<Map<String,Object>> lstScript = ss.selectList(SID_LOAD_ALL_SCRIPT);
        lstScript.forEach(this::parseScriptConfig);
    }

    /**
     * 定时从数据库更新
     */
    public void startSyncTask(){
        if(!mep.isEnableSyncTask()){
            log.info("startSyncTask syncTask unable");
            return ;
        }
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        Runnable task = () -> {
            try {
                Optional<Long> mv = versionMap.values().stream().max(Long::compareTo);
                if (!mv.isPresent()) {
                    return;
                }
                log.info("startSyncTask start version:{}", mv.get());
                List<Map<String, Object>> lstScript = ss.selectList(SID_LOAD_SCRIPT_BY_VERSION, mv.get());
                lstScript.forEach(this::parseScriptConfig);
                log.info("startSyncTask end version:{} update config size:{}", mv.get(), lstScript.size());
            }catch (Exception e){
                log.error("startSyncTask error:",e);
            }
        };
        executor.scheduleAtFixedRate(task, 0, 10, TimeUnit.SECONDS);
    }
}
